package pub.gll.plugin.asm.annotation;


import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Opcodes;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import pub.gll.plugin.asm.model.RuleConfigManager;
import pub.gll.plugin.asm.model.Source;
import pub.gll.plugin.asm.model.Target;
import pub.gll.plugin.util.ClassUtil;
import pub.gll.plugin.util.CloseUtil;
import pub.gll.plugin.util.Log;
import pub.gll.plugin.util.RegexUtil;

/**
 * @author yuqiang
 */
public class AspectAnnotationVisitor extends AnnotationVisitor implements Opcodes {
    /**
     * {@link pub.gll.libaptannotation.Around#enter()}
     * {@link pub.gll.libaptannotation.Timing#enter()}
     */
    private static final String ANNOTATION_ENTER = "enter";


    /**
     * {@link pub.gll.libaptannotation.TryCatch#extend()}
     * {@link pub.gll.libaptannotation.Interceptor#extend()}
     * {@link pub.gll.libaptannotation.Around#extend()}
     * {@link pub.gll.libaptannotation.Timing#extend()}
     * {@link pub.gll.libaptannotation.Replace#extend()}
     */
    private static final String ANNOTATION_EXTEND = "extend";
    private static final String ANNOTATION_TARGET = "target";
    private static final String ANNOTATION_DEBUG = "debug";
    private static final String ANNOTATION_SOURCE_INFO = "needSourceInfo";
    private static final String ANNOTATION_FORCE_VERIFY = "forceVerify";

    /**
     * {@link pub.gll.libaptannotation.Interceptor#returnValue()}
     */
    private static final String ANNOTATION_RETURN_VALUE = "returnValue";
    /**
     * {@link pub.gll.libaptannotation.Replace#extend()}
     */
    private static final String ANNOTATION_CALL_INFO = "caller";

    final private List<Source> sourceArray;
    final private List<String> callerInfoList;
    final private String annotationDesc;
    final private Target target;

    private boolean methodEnter = true;
    private boolean extend = false;
    private boolean returnValue = false;
    private boolean debug = false;
    private boolean needSourceInfo = false;
    private boolean forceVerify = true;
    private CopyOnWriteArrayList<Runnable> pendingListAction = new CopyOnWriteArrayList<>();

    AspectAnnotationVisitor(String annotationDesc, Target target) {
        super(ASM5);
        this.annotationDesc = annotationDesc;
        this.target = target;
        sourceArray = new CopyOnWriteArrayList<>();
        callerInfoList = new CopyOnWriteArrayList<>();
    }

    @Override
    public AnnotationVisitor visitArray(String name) {
        AnnotationVisitor av = super.visitArray(name);
        if (ANNOTATION_CALL_INFO.equals(name)) {
            return new CallerArrayAnnotationVisitor(av);
        }
        return new TargetArrayAnnotationVisitor(av, name);
    }

    @Override
    public void visit(String name, Object value) {
        super.visit(name, value);
        if (ANNOTATION_ENTER.equals(name)) {
            methodEnter = (boolean) value;
        }
        if (ANNOTATION_DEBUG.equals(name)) {
            debug = (boolean) value;
        }
        if (ANNOTATION_EXTEND.equals(name)) {
            extend = (boolean) value;
        }
        if (ANNOTATION_RETURN_VALUE.equals(name)) {
            returnValue = (boolean) value;
        }
        if (ANNOTATION_SOURCE_INFO.equals(name)) {
            needSourceInfo = (boolean)value;
        }
        if (ANNOTATION_FORCE_VERIFY.equals(name)) {
            forceVerify = (boolean) value;
        }
    }

    @Override
    public void visitEnd() {
        super.visitEnd();
        if (!pendingListAction.isEmpty()) {
            for (Runnable runnable: pendingListAction) {
                runnable.run();
            }
            pendingListAction.clear();
        }
        if (!CollectArchBindingDataContainer.isDebug() && debug) {
            StringBuilder sb = new StringBuilder();
            if (target != null) {
                sb.append("className is -> ").append(target.getClassName());
                sb.append(", methodName is -> ").append(target.getMethodName());
                sb.append(", source array size ->").append(sourceArray.size());
            }
            Log.d("AspectCollect", "filter debug annotation -> " + sb.toString());
            return;
        }
        for (Source source : sourceArray) {
            //for source
            source.setExtend(extend);
            source.setAnnotationDesc(annotationDesc);
            //for target
            target.setMethodEnter(methodEnter);
            target.setReturnValue(returnValue);
            target.setCallerInfoList(callerInfoList);
            target.setNeedSourceInfo(needSourceInfo);
            target.setForceVerify(forceVerify);
            RuleConfigManager.getInstance().add(source, target);
        }
    }

    class CallerArrayAnnotationVisitor extends AnnotationVisitor {

        private CallerArrayAnnotationVisitor(AnnotationVisitor av) {
            super(Opcodes.ASM7, av);
        }

        @Override
        public void visit(String name, Object value) {
            super.visit(name, value);
            callerInfoList.add(String.valueOf(value));
        }
    }

    class TargetArrayAnnotationVisitor extends AnnotationVisitor {

        final String name;

        private TargetArrayAnnotationVisitor(AnnotationVisitor av, String name) {
            super(Opcodes.ASM7, av);
            this.name = name;
        }

        @Override
        public void visit(String name, Object value) {
            super.visit(name, value);
            pendingListAction.add(new Runnable() {
                @Override
                public void run() {
                    handleTypeAnnotation(name, value);
                }
            });

        }

        private void handleTypeAnnotation(String name, Object value) {
            String propValue = String.valueOf(value);
            String className = propValue.substring(0, propValue.lastIndexOf("."));
            String methodName = propValue.substring(propValue.lastIndexOf(".") + 1, propValue.indexOf("("));
            String methodDesc = propValue.substring(propValue.indexOf("("), propValue.length());

            if (!ClassUtil.isMethodExist(propValue, className, methodName, methodDesc)) {
                if (forceVerify) {
                    CloseUtil.exit(propValue + " is not exists !");
                }
            }
            Source source = new Source.Builder()
                    .className(RegexUtil.replace(className))
                    .methodName(RegexUtil.replace(methodName))
                    .methodDesc(RegexUtil.replace(methodDesc))
                    .build();
            if (!sourceArray.contains(source)) {
                sourceArray.add(source);
            }
        }
    }
}
